/*
 * nicolas.bredeche(at)lri.fr
 * 20070519
 */

package piconode.core.node;

import java.util.ArrayList;

import piconode.core.arc.ConstrainedArc;
import piconode.core.arc.LearnableArc;
import piconode.toolbox.Display;

/**
 * implementation of the basics for creating echo state networks, a
 * discrete-time continuous state recurrent neural networks with only a sub-part
 * of the total number of weights that can be learnt. Check out the
 * corresponding Factory class for automatic creation of ESN.
 * 
 * @author nicolas
 * 
 */
public class EchoStateNetwork extends RecurrentNeuralNetwork {

	protected ArrayList _learnableArcList = new ArrayList(); // contains
	// learnable
	// arcs (note:
	// contains
	// *pointers* to
	// arcs already
	// in the
	// standard
	// list) - used
	// for targetted
	// learning/optimization
	private double _noiseValue = 0;

	private int _nbIn, _nbOut, _reservoirSize;
	private boolean _outputTanhFn = false;
	private boolean _inputOutputDirectConnections = false;
	private boolean _outputSelfRecurrentConnections = false;

	public void setNbIn(int __nbIn) {
		_nbIn = __nbIn;
	}

	public void setNbOut(int __nbOut) {
		_nbOut = __nbOut;
	}

	public void setReservoirSize(int __reservoirSize) {
		_reservoirSize = __reservoirSize;
	}

	public void setOutputActivationFunction(boolean __outputTanhActivationFunction) {
		_outputTanhFn = __outputTanhActivationFunction;
	}

	public void setInputOutputDirectConnections(boolean __inputToOutputDirectConnections) {
		_inputOutputDirectConnections = __inputToOutputDirectConnections;
	}

	public void setOutputSelfRecurrentConnections(boolean __outputSelfRecurrentConnections) {
		_outputSelfRecurrentConnections = __outputSelfRecurrentConnections;
	}

	public int getNbIn() {
		return _nbIn;
	}

	public int getNbOut() {
		return _nbOut;
	}

	public int getReservoirSize() {
		return _reservoirSize;
	}

	public boolean getOutputActivationFunction() {
		return _outputTanhFn;
	}

	public boolean getInputOutputDirectConnections() {
		return _inputOutputDirectConnections;
	}

	public boolean getOutputSelfRecurrentConnections() {
		return _outputSelfRecurrentConnections;
	}

	/**
	 * return a list with weights values from all arcs (same as
	 * getWeightsFromStdArcs())
	 * 
	 * @return
	 */
	public ArrayList getWeightsFromLearningArcs() {
		ArrayList valuesList = new ArrayList();

		for (int i = 0; i != getLearnableArcListSize(); i++)
			valuesList.add(new Double(((LearnableArc) this._learnableArcList.get(i)).getWeightValue()));

		return (valuesList);
	}

	public void setLearningArcsWeightValues(ArrayList __list) {
		for (int i = 0; i != getLearnableArcListSize(); i++)
			((LearnableArc) (this._learnableArcList.get(i))).setWeightValue(((Double) (__list.get(i))).doubleValue());
	}

	public int getLearnableArcListSize() {
		return this._learnableArcList.size();
	}

	public void registerLearnableArc(ConstrainedArc __arc) {
		if (__arc.getClass() != LearnableArc.class)
			Display.critical("EchoStateNetwork::registerArc(.) - only LearnableArc object can be registered.");
		else {
			this._learnableArcList.add(__arc);
			super.registerArc(__arc);
		}
	}

	/**
	 * initialize reccurrent neural network and update list of learning-enable
	 * arcs.
	 */
	@Override
	public void initNetwork() {
		// default init
		super.initNetwork();

		// check and record learning-enable arcs (for accelerating future
		// access).
		for (int i = 0; i != this.getArcListSize(); i++) {
			if (((LearnableArc) this.getArcAt(i)).isLearning())
				this._learnableArcList.add(this.getArcAt(i));
		}
	}

	/**
	 * perform one iteration step using input data from incoming nodes if any --
	 * arity must be correct ESN update rule is slightly different from
	 * piconode.RecurrentNeuralNetwork (all nodes are updated synchroneously) :
	 * 0. add noise to output neurons 1. update reservoir 2. update output (i.e.
	 * from reservoir at t+1)
	 * 
	 * @param __valuesList
	 */
	@Override
	public void step() {
		// System.out.println("checkpoint.");

		if (_init == false) {
			System.out.println("EchosStateNetwork::step() - network not initialized!");
			System.exit(-1);
		}

		// add noise to output nodes values
		for (int i = this._nodeList.size() - this._outputNeuronsList.size(); i != this._nodeList.size(); i++) {
			((RecurrentNeuron) this._nodeList.get(i)).setInputValue(((RecurrentNeuron) this._nodeList.get(i)).getValue() + randomNoise());
		}

		// feed-forward signal to compute output *new* neurons values.
		// current neuron values are unaltered and new values are "buffered".
		// do not compute neuron values neither for input nodes (never) nor for
		// output nodes (later)
		for (int i = this._inputNeuronsList.size(); i != this._nodeList.size() - this._outputNeuronsList.size(); i++) {
			// System.out.println("! "+
			// ((RecurrentNeuron)this._nodeList.get(i)).getName() +" !");
			((RecurrentNeuron) this._nodeList.get(i)).step();
		}

		// for every neuron, copy the new "buffered" value to current value
		this.updateReservoir();

		// compute output nodes values
		for (int i = this._nodeList.size() - this._outputNeuronsList.size(); i != this._nodeList.size(); i++) {
			// System.out.println("! " +
			// ((RecurrentNeuron)this._nodeList.get(i)).getName() + " !");
			((RecurrentNeuron) this._nodeList.get(i)).step();
		}

		this.updateOutputNodes();

		// recopy actual output values as old output values

		for (int i = 0; i < getOutputRegisterSize(); i++)
			setOldOutputRegister(i, getOutputRegister(i));
	}

	/**
	 * update all nodes in the reservoir - used in the step() method
	 */

	private void updateReservoir() {
		for (int i = this.getInputNeuronListSize(); i != this.getNodeListSize() - this.getOutputNeuronListSize(); i++)
			((RecurrentNeuron) this.getNodeAt(i)).updateValue();
	}

	/**
	 * update all output nodes - used in the step() method
	 */

	private void updateOutputNodes() {
		for (int i = this.getNodeListSize() - this.getOutputNeuronListSize(); i != this.getNodeListSize(); i++)
			((RecurrentNeuron) this.getNodeAt(i)).updateValue();
	}

	/**
	 * get random noise from uniform distribution around zero. default range is
	 * zero (refer to setNoiseValue method to set +-value)
	 * 
	 * @return random value centered on zero with range specified (if range is
	 *         zero, return value is zero)
	 */
	private double randomNoise() {
		if (this._noiseValue == 0)
			return 0;
		else
			return ((2.d * Math.random() - 1.0) * this._noiseValue);
	}

	/**
	 * set noise +-value for randomNoise method.
	 * 
	 * @param value
	 *            (e.g. 0.1 for [-0.01,+0.01])
	 */
	public void setNoiseValue(double value) {
		_noiseValue = value;
	}

	/**
	 * return the number of arcs projecting onto output nodes.
	 * 
	 * @return
	 */
	public int getOutputNodesAfferentArcWeightsListSize() {
		int size = 0;
		for (int i = 0; i != this.getNbOut(); i++)
			size += this.getOutputNeuronAt(i).getIncomingArcsList().size();
		return size;
	}

	/**
	 * return a list containing the weight values of all arcs projecting onto
	 * output nodes. Useful for checking learned weights.
	 * 
	 * @return
	 */
	public double[] getOutputNodesAfferentArcWeights() {
		double[] values = new double[this.getOutputNodesAfferentArcWeightsListSize()];

		for (int i = 0, k = 0; i != this.getNbOut(); i++)
			for (int j = 0; j != this.getOutputNeuronAt(i).getIncomingArcsList().size(); j++, k++)
				values[k] = ((LearnableArc) this.getOutputNeuronAt(i).getIncomingArcAt(j)).getWeightValue();

		return values;
	}

	/**
	 * set weight values for all arcs projecting onto output nodes. Particularly
	 * useful when learning or optimizing (e.g. genotype encodes these values in
	 * an EA context).
	 * 
	 * @param values
	 */
	public void setOutputNodesAfferentArcWeights(double[] values) {
		if (values.length != this.getOutputNodesAfferentArcWeightsListSize()) {
			System.err.println(this.getClass().getName() + ".setOutputNodesAfferentArcWeights(.) - list sizes do not match (waiting for " + this.getOutputNodesAfferentArcWeightsListSize() + ", given " + values.length + ")");
			System.exit(-1);
		}

		for (int i = 0, k = 0; i != this.getNbOut(); i++)
			for (int j = 0; j != this.getOutputNeuronAt(i).getIncomingArcsList().size(); j++, k++)
				((LearnableArc) this.getOutputNeuronAt(i).getIncomingArcAt(j)).setWeightValue(values[k]);
	}

}
